1   /*
2    * Copyright (c) 2002, 2010, Oracle and/or its affiliates. All rights reserved.
3    * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4    *
5    * This code is free software; you can redistribute it and/or modify it
6    * under the terms of the GNU General Public License version 2 only, as
7    * published by the Free Software Foundation.  Oracle designates this
8    * particular file as subject to the "Classpath" exception as provided
9    * by Oracle in the LICENSE file that accompanied this code.
10   *
11   * This code is distributed in the hope that it will be useful, but WITHOUT
12   * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13   * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14   * version 2 for more details (a copy is included in the LICENSE file that
15   * accompanied this code).
16   *
17   * You should have received a copy of the GNU General Public License version
18   * 2 along with this work; if not, write to the Free Software Foundation,
19   * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20   *
21   * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22   * or visit www.oracle.com if you need additional information or have any
23   * questions.
24   */
25  
26  package com.sun.jmx.remote.internal;
27  
28  import com.sun.jmx.mbeanserver.Util;
29  import com.sun.jmx.remote.security.NotificationAccessController;
30  import com.sun.jmx.remote.util.ClassLogger;
31  import com.sun.jmx.remote.util.EnvHelp;
32  import java.io.IOException;
33  import java.security.AccessControlContext;
34  import java.security.AccessController;
35  import java.security.PrivilegedAction;
36  import java.security.PrivilegedActionException;
37  import java.security.PrivilegedExceptionAction;
38  import java.util.Collections;
39  import java.util.HashMap;
40  import java.util.HashSet;
41  import java.util.List;
42  import java.util.Map;
43  import java.util.Set;
44  import javax.management.InstanceNotFoundException;
45  import javax.management.ListenerNotFoundException;
46  import javax.management.MBeanPermission;
47  import javax.management.MBeanServer;
48  import javax.management.MBeanServerDelegate;
49  import javax.management.MBeanServerNotification;
50  import javax.management.Notification;
51  import javax.management.NotificationBroadcaster;
52  import javax.management.NotificationFilter;
53  import javax.management.ObjectInstance;
54  import javax.management.ObjectName;
55  import javax.management.remote.NotificationResult;
56  import javax.management.remote.TargetedNotification;
57  import javax.management.MalformedObjectNameException;
58  import javax.security.auth.Subject;
59  
60  public class ServerNotifForwarder {
61  
62  
63      public ServerNotifForwarder(MBeanServer mbeanServer,
64                                  Map<String, ?> env,
65                                  NotificationBuffer notifBuffer,
66                                  String connectionId) {
67          this.mbeanServer = mbeanServer;
68          this.notifBuffer = notifBuffer;
69          this.connectionId = connectionId;
70          connectionTimeout = EnvHelp.getServerConnectionTimeout(env);
71          checkNotificationEmission = EnvHelp.computeBooleanFromString(
72              env,
73              "jmx.remote.x.check.notification.emission",false);
74          notificationAccessController =
75                  EnvHelp.getNotificationAccessController(env);
76      }
77  
78      public Integer addNotificationListener(final ObjectName name,
79          final NotificationFilter filter)
80          throws InstanceNotFoundException, IOException {
81  
82          if (logger.traceOn()) {
83              logger.trace("addNotificationListener",
84                  "Add a listener at " + name);
85          }
86  
87          checkState();
88  
89          // Explicitly check MBeanPermission for addNotificationListener
90          //
91          checkMBeanPermission(name, "addNotificationListener");
92          if (notificationAccessController != null) {
93              notificationAccessController.addNotificationListener(
94                  connectionId, name, getSubject());
95          }
96          try {
97              boolean instanceOf =
98              AccessController.doPrivileged(
99                      new PrivilegedExceptionAction<Boolean>() {
100                         public Boolean run() throws InstanceNotFoundException {
101                             return mbeanServer.isInstanceOf(name, broadcasterClass);
102                         }
103             });
104             if (!instanceOf) {
105                 throw new IllegalArgumentException("The specified MBean [" +
106                     name + "] is not a " +
107                     "NotificationBroadcaster " +
108                     "object.");
109             }
110         } catch (PrivilegedActionException e) {
111             throw (InstanceNotFoundException) extractException(e);
112         }
113 
114         final Integer id = getListenerID();
115 
116         // 6238731: set the default domain if no domain is set.
117         ObjectName nn = name;
118         if (name.getDomain() == null || name.getDomain().equals("")) {
119             try {
120                 nn = ObjectName.getInstance(mbeanServer.getDefaultDomain(),
121                                             name.getKeyPropertyList());
122             } catch (MalformedObjectNameException mfoe) {
123                 // impossible, but...
124                 IOException ioe = new IOException(mfoe.getMessage());
125                 ioe.initCause(mfoe);
126                 throw ioe;
127             }
128         }
129 
130         synchronized (listenerMap) {
131             IdAndFilter idaf = new IdAndFilter(id, filter);
132             Set<IdAndFilter> set = listenerMap.get(nn);
133             // Tread carefully because if set.size() == 1 it may be the
134             // Collections.singleton we make here, which is unmodifiable.
135             if (set == null)
136                 set = Collections.singleton(idaf);
137             else {
138                 if (set.size() == 1)
139                     set = new HashSet<IdAndFilter>(set);
140                 set.add(idaf);
141             }
142             listenerMap.put(nn, set);
143         }
144 
145         return id;
146     }
147 
148     public void removeNotificationListener(ObjectName name,
149         Integer[] listenerIDs)
150         throws Exception {
151 
152         if (logger.traceOn()) {
153             logger.trace("removeNotificationListener",
154                 "Remove some listeners from " + name);
155         }
156 
157         checkState();
158 
159         // Explicitly check MBeanPermission for removeNotificationListener
160         //
161         checkMBeanPermission(name, "removeNotificationListener");
162         if (notificationAccessController != null) {
163             notificationAccessController.removeNotificationListener(
164                 connectionId, name, getSubject());
165         }
166 
167         Exception re = null;
168         for (int i = 0 ; i < listenerIDs.length ; i++) {
169             try {
170                 removeNotificationListener(name, listenerIDs[i]);
171             } catch (Exception e) {
172                 // Give back the first exception
173                 //
174                 if (re != null) {
175                     re = e;
176                 }
177             }
178         }
179         if (re != null) {
180             throw re;
181         }
182     }
183 
184     public void removeNotificationListener(ObjectName name, Integer listenerID)
185     throws
186         InstanceNotFoundException,
187         ListenerNotFoundException,
188         IOException {
189 
190         if (logger.traceOn()) {
191             logger.trace("removeNotificationListener",
192                 "Remove the listener " + listenerID + " from " + name);
193         }
194 
195         checkState();
196 
197         if (name != null && !name.isPattern()) {
198             if (!mbeanServer.isRegistered(name)) {
199                 throw new InstanceNotFoundException("The MBean " + name +
200                     " is not registered.");
201             }
202         }
203 
204         synchronized (listenerMap) {
205             // Tread carefully because if set.size() == 1 it may be a
206             // Collections.singleton, which is unmodifiable.
207             Set<IdAndFilter> set = listenerMap.get(name);
208             IdAndFilter idaf = new IdAndFilter(listenerID, null);
209             if (set == null || !set.contains(idaf))
210                 throw new ListenerNotFoundException("Listener not found");
211             if (set.size() == 1)
212                 listenerMap.remove(name);
213             else
214                 set.remove(idaf);
215         }
216     }
217 
218     /* This is the object that will apply our filtering to candidate
219      * notifications.  First of all, if there are no listeners for the
220      * ObjectName that the notification is coming from, we go no further.
221      * Then, for each listener, we must apply the corresponding filter (if any)
222      * and ignore the listener if the filter rejects.  Finally, we apply
223      * some access checks which may also reject the listener.
224      *
225      * A given notification may trigger several listeners on the same MBean,
226      * which is why listenerMap is a Map<ObjectName, Set<IdAndFilter>> and
227      * why we add the found notifications to a supplied List rather than
228      * just returning a boolean.
229      */
230     private final NotificationBufferFilter bufferFilter =
231             new NotificationBufferFilter() {
232         public void apply(List<TargetedNotification> targetedNotifs,
233                           ObjectName source, Notification notif) {
234             // We proceed in two stages here, to avoid holding the listenerMap
235             // lock while invoking the filters (which are user code).
236             final IdAndFilter[] candidates;
237             synchronized (listenerMap) {
238                 final Set<IdAndFilter> set = listenerMap.get(source);
239                 if (set == null) {
240                     logger.debug("bufferFilter", "no listeners for this name");
241                     return;
242                 }
243                 candidates = new IdAndFilter[set.size()];
244                 set.toArray(candidates);
245             }
246             // We don't synchronize on targetedNotifs, because it is a local
247             // variable of our caller and no other thread can see it.
248             for (IdAndFilter idaf : candidates) {
249                 final NotificationFilter nf = idaf.getFilter();
250                 if (nf == null || nf.isNotificationEnabled(notif)) {
251                     logger.debug("bufferFilter", "filter matches");
252                     final TargetedNotification tn =
253                             new TargetedNotification(notif, idaf.getId());
254                     if (allowNotificationEmission(source, tn))
255                         targetedNotifs.add(tn);
256                 }
257             }
258         }
259     };
260 
261     public NotificationResult fetchNotifs(long startSequenceNumber,
262         long timeout,
263         int maxNotifications) {
264         if (logger.traceOn()) {
265             logger.trace("fetchNotifs", "Fetching notifications, the " +
266                 "startSequenceNumber is " + startSequenceNumber +
267                 ", the timeout is " + timeout +
268                 ", the maxNotifications is " + maxNotifications);
269         }
270 
271         NotificationResult nr;
272         final long t = Math.min(connectionTimeout, timeout);
273         try {
274             nr = notifBuffer.fetchNotifications(bufferFilter,
275                 startSequenceNumber,
276                 t, maxNotifications);
277             snoopOnUnregister(nr);
278         } catch (InterruptedException ire) {
279             nr = new NotificationResult(0L, 0L, new TargetedNotification[0]);
280         }
281 
282         if (logger.traceOn()) {
283             logger.trace("fetchNotifs", "Forwarding the notifs: "+nr);
284         }
285 
286         return nr;
287     }
288 
289     // The standard RMI connector client will register a listener on the MBeanServerDelegate
290     // in order to be told when MBeans are unregistered.  We snoop on fetched notifications
291     // so that we can know too, and remove the corresponding entry from the listenerMap.
292     // See 6957378.
293     private void snoopOnUnregister(NotificationResult nr) {
294         Set<IdAndFilter> delegateSet = listenerMap.get(MBeanServerDelegate.DELEGATE_NAME);
295         if (delegateSet == null || delegateSet.isEmpty()) {
296             return;
297         }
298         for (TargetedNotification tn : nr.getTargetedNotifications()) {
299             Integer id = tn.getListenerID();
300             for (IdAndFilter idaf : delegateSet) {
301                 if (idaf.id == id) {
302                     // This is a notification from the MBeanServerDelegate.
303                     Notification n = tn.getNotification();
304                     if (n instanceof MBeanServerNotification &&
305                             n.getType().equals(MBeanServerNotification.UNREGISTRATION_NOTIFICATION)) {
306                         MBeanServerNotification mbsn = (MBeanServerNotification) n;
307                         ObjectName gone = mbsn.getMBeanName();
308                         synchronized (listenerMap) {
309                             listenerMap.remove(gone);
310                         }
311                     }
312                 }
313             }
314         }
315     }
316 
317     public void terminate() {
318         if (logger.traceOn()) {
319             logger.trace("terminate", "Be called.");
320         }
321 
322         synchronized(terminationLock) {
323             if (terminated) {
324                 return;
325             }
326 
327             terminated = true;
328 
329             synchronized(listenerMap) {
330                 listenerMap.clear();
331             }
332         }
333 
334         if (logger.traceOn()) {
335             logger.trace("terminate", "Terminated.");
336         }
337     }
338 
339     //----------------
340     // PRIVATE METHODS
341     //----------------
342 
343     private Subject getSubject() {
344         return Subject.getSubject(AccessController.getContext());
345     }
346 
347     private void checkState() throws IOException {
348         synchronized(terminationLock) {
349             if (terminated) {
350                 throw new IOException("The connection has been terminated.");
351             }
352         }
353     }
354 
355     private Integer getListenerID() {
356         synchronized(listenerCounterLock) {
357             return listenerCounter++;
358         }
359     }
360 
361     /**
362      * Explicitly check the MBeanPermission for
363      * the current access control context.
364      */
365     public void checkMBeanPermission(
366             final ObjectName name, final String actions)
367             throws InstanceNotFoundException, SecurityException {
368         SecurityManager sm = System.getSecurityManager();
369         if (sm != null) {
370             AccessControlContext acc = AccessController.getContext();
371             ObjectInstance oi;
372             try {
373                 oi = AccessController.doPrivileged(
374                     new PrivilegedExceptionAction<ObjectInstance>() {
375                         public ObjectInstance run()
376                         throws InstanceNotFoundException {
377                             return mbeanServer.getObjectInstance(name);
378                         }
379                 });
380             } catch (PrivilegedActionException e) {
381                 throw (InstanceNotFoundException) extractException(e);
382             }
383             String classname = oi.getClassName();
384             MBeanPermission perm = new MBeanPermission(
385                 classname,
386                 null,
387                 name,
388                 actions);
389             sm.checkPermission(perm, acc);
390         }
391     }
392 
393     /**
394      * Check if the caller has the right to get the following notifications.
395      */
396     private boolean allowNotificationEmission(ObjectName name,
397                                               TargetedNotification tn) {
398         try {
399             if (checkNotificationEmission) {
400                 checkMBeanPermission(name, "addNotificationListener");
401             }
402             if (notificationAccessController != null) {
403                 notificationAccessController.fetchNotification(
404                         connectionId, name, tn.getNotification(), getSubject());
405             }
406             return true;
407         } catch (SecurityException e) {
408             if (logger.debugOn()) {
409                 logger.debug("fetchNotifs", "Notification " +
410                         tn.getNotification() + " not forwarded: the " +
411                         "caller didn't have the required access rights");
412             }
413             return false;
414         } catch (Exception e) {
415             if (logger.debugOn()) {
416                 logger.debug("fetchNotifs", "Notification " +
417                         tn.getNotification() + " not forwarded: " +
418                         "got an unexpected exception: " + e);
419             }
420             return false;
421         }
422     }
423 
424     /**
425      * Iterate until we extract the real exception
426      * from a stack of PrivilegedActionExceptions.
427      */
428     private static Exception extractException(Exception e) {
429         while (e instanceof PrivilegedActionException) {
430             e = ((PrivilegedActionException)e).getException();
431         }
432         return e;
433     }
434 
435     private static class IdAndFilter {
436         private Integer id;
437         private NotificationFilter filter;
438 
439         IdAndFilter(Integer id, NotificationFilter filter) {
440             this.id = id;
441             this.filter = filter;
442         }
443 
444         Integer getId() {
445             return this.id;
446         }
447 
448         NotificationFilter getFilter() {
449             return this.filter;
450         }
451 
452         @Override
453         public int hashCode() {
454             return id.hashCode();
455         }
456 
457         @Override
458         public boolean equals(Object o) {
459             return ((o instanceof IdAndFilter) &&
460                     ((IdAndFilter) o).getId().equals(getId()));
461         }
462     }
463 
464 
465     //------------------
466     // PRIVATE VARIABLES
467     //------------------
468 
469     private MBeanServer mbeanServer;
470 
471     private final String connectionId;
472 
473     private final long connectionTimeout;
474 
475     private static int listenerCounter = 0;
476     private final static int[] listenerCounterLock = new int[0];
477 
478     private NotificationBuffer notifBuffer;
479     private final Map<ObjectName, Set<IdAndFilter>> listenerMap =
480             new HashMap<ObjectName, Set<IdAndFilter>>();
481 
482     private boolean terminated = false;
483     private final int[] terminationLock = new int[0];
484 
485     static final String broadcasterClass =
486         NotificationBroadcaster.class.getName();
487 
488     private final boolean checkNotificationEmission;
489 
490     private final NotificationAccessController notificationAccessController;
491 
492     private static final ClassLogger logger =
493         new ClassLogger("javax.management.remote.misc", "ServerNotifForwarder");
494 }